2 * Adium is the legal property of its developers, whose names are listed in the copyright file included
3 * with this source distribution.
5 * This program is free software; you can redistribute it and/or modify it under the terms of the GNU
6 * General Public License as published by the Free Software Foundation; either version 2 of the License,
7 * or (at your option) any later version.
9 * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
10 * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
11 * Public License for more details.
13 * You should have received a copy of the GNU General Public License along with this program; if not,
14 * write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
17 #import "AIMessageViewController.h"
18 #import "AIAccountSelectionView.h"
19 #import "AIMessageWindowController.h"
20 #import "ESGeneralPreferencesPlugin.h"
21 #import "AIDualWindowInterfacePlugin.h"
22 #import "AIContactInfoWindowController.h"
23 #import "AIMessageTabSplitView.h"
25 #import <Adium/AIChatControllerProtocol.h>
26 #import <Adium/AIContactAlertsControllerProtocol.h>
27 #import <Adium/AIContactControllerProtocol.h>
28 #import <Adium/AIContentControllerProtocol.h>
29 #import <Adium/AIContentControllerProtocol.h>
30 #import <Adium/AIInterfaceControllerProtocol.h>
31 #import <Adium/AIMenuControllerProtocol.h>
32 #import <Adium/AIPreferenceControllerProtocol.h>
33 #import <Adium/AIToolbarControllerProtocol.h>
34 #import <Adium/AIAccount.h>
35 #import <Adium/AIChat.h>
36 #import <Adium/AIContentMessage.h>
37 #import <Adium/AIListContact.h>
38 #import <Adium/AIListObject.h>
39 #import <Adium/AIListOutlineView.h>
40 #import <Adium/AIMessageEntryTextView.h>
41 #import <Adium/ESTextAndButtonsWindowController.h>
43 #import <AIUtilities/AIApplicationAdditions.h>
44 #import <AIUtilities/AIAttributedStringAdditions.h>
45 #import <AIUtilities/AIAutoScrollView.h>
46 #import <AIUtilities/AIDictionaryAdditions.h>
47 #import <AIUtilities/AISplitView.h>
49 #import <AIUtilities/AITigerCompatibility.h>
51 #import <PSMTabBarControl/NSBezierPath_AMShading.h>
52 #import "KNShelfSplitView.h"
53 #import "ESChatUserListController.h"
56 #define MESSAGE_VIEW_MIN_HEIGHT_RATIO .50 //Mininum height ratio of the message view
57 #define MESSAGE_VIEW_MIN_WIDTH_RATIO .50 //Mininum width ratio of the message view
58 #define ENTRY_TEXTVIEW_MIN_HEIGHT 20 //Mininum height of the text entry view
59 #define USER_LIST_MIN_WIDTH 24 //Mininum width of the user list
60 #define USER_LIST_DEFAULT_WIDTH 120 //Default width of the user list
62 //Preferences and files
63 #define MESSAGE_VIEW_NIB @"MessageView" //Filename of the message view nib
64 #define USERLIST_THEME @"UserList Theme" //File name of the user list theme
65 #define USERLIST_LAYOUT @"UserList Layout" //File name of the user list layout
66 #define KEY_ENTRY_TEXTVIEW_MIN_HEIGHT @"Minimum Text Height" //Preference key for text entry height
67 #define KEY_ENTRY_USER_LIST_MIN_WIDTH @"UserList Width" //Preference key for user list width
70 @interface AIMessageViewController (PRIVATE)
71 - (id)initForChat:(AIChat *)inChat;
72 - (void)chatStatusChanged:(NSNotification *)notification;
73 - (void)chatParticipatingListObjectsChanged:(NSNotification *)notification;
74 - (void)_configureMessageDisplay;
75 - (void)_createAccountSelectionView;
76 - (void)_destroyAccountSelectionView;
77 - (void)_configureTextEntryView;
78 - (void)_updateTextEntryViewHeight;
79 - (int)_textEntryViewProperHeightIgnoringUserMininum:(BOOL)ignoreUserMininum;
80 - (void)_showUserListView;
81 - (void)_hideUserListView;
82 - (void)_configureUserList;
83 - (void)_updateUserListViewWidth;
84 - (int)_userListViewProperWidthIgnoringUserMininum:(BOOL)ignoreUserMininum;
85 - (void)updateFramesForAccountSelectionView;
86 - (void)saveUserListMinimumSize;
89 @implementation AIMessageViewController
92 * @brief Create a new message view controller
94 + (AIMessageViewController *)messageDisplayControllerForChat:(AIChat *)inChat
96 return [[[self alloc] initForChat:inChat] autorelease];
103 - (id)initForChat:(AIChat *)inChat
105 if ((self = [super init])) {
106 AIListContact *contact;
108 chat = [inChat retain];
109 contact = [chat listObject];
110 view_accountSelection = nil;
111 userListController = nil;
112 suppressSendLaterPrompt = NO;
113 retainingScrollViewUserList = NO;
115 //Load the view containing our controls
116 [NSBundle loadNibNamed:MESSAGE_VIEW_NIB owner:self];
118 //Register for the various notification we need
119 [[adium notificationCenter] addObserver:self
120 selector:@selector(sendMessage:)
121 name:Interface_SendEnteredMessage
123 [[adium notificationCenter] addObserver:self
124 selector:@selector(didSendMessage:)
125 name:Interface_DidSendEnteredMessage
127 [[adium notificationCenter] addObserver:self
128 selector:@selector(chatStatusChanged:)
129 name:Chat_StatusChanged
131 [[adium notificationCenter] addObserver:self
132 selector:@selector(chatParticipatingListObjectsChanged:)
133 name:Chat_ParticipatingListObjectsChanged
135 [[adium notificationCenter] addObserver:self
136 selector:@selector(redisplaySourceAndDestinationSelector:)
137 name:Chat_SourceChanged
139 [[adium notificationCenter] addObserver:self
140 selector:@selector(redisplaySourceAndDestinationSelector:)
141 name:Chat_DestinationChanged
143 [[adium notificationCenter] addObserver:self
144 selector:@selector(toggleUserlist:)
145 name:@"toggleUserlist"
148 [splitView_textEntryHorizontal setDividerThickness:3]; //Default is 9
149 [splitView_textEntryHorizontal setDrawsDivider:NO];
151 //Observe general preferences for sending keys
152 [[adium preferenceController] registerPreferenceObserver:self forGroup:PREF_GROUP_GENERAL];
154 /* Update chat status and participating list objects to configure the user list if necessary
155 * Call chatParticipatingListObjectsChanged first, which will set up the user list. This allows other sizing to match.
157 [self setUserListVisible:[chat isGroupChat]];
159 [self chatParticipatingListObjectsChanged:nil];
160 [self chatStatusChanged:nil];
162 //Configure our views
163 [self _configureMessageDisplay];
164 [self _configureTextEntryView];
166 //Set our base writing direction
168 [textView_outgoing setBaseWritingDirection:[contact baseWritingDirection]];
180 AIListContact *contact = [chat listObject];
182 [[adium preferenceController] unregisterPreferenceObserver:self];
184 //Store our minimum height for the text entry area, and minimim width for the user list
185 [[adium preferenceController] setPreference:[NSNumber numberWithInt:entryMinHeight]
186 forKey:KEY_ENTRY_TEXTVIEW_MIN_HEIGHT
187 group:PREF_GROUP_DUAL_WINDOW_INTERFACE];
189 if (userListController) {
190 [self saveUserListMinimumSize];
193 //Save the base writing direction
195 [contact setBaseWritingDirection:[textView_outgoing baseWritingDirection]];
197 [chat release]; chat = nil;
200 [[adium notificationCenter] removeObserver:self];
201 [[NSNotificationCenter defaultCenter] removeObserver:self];
203 //Account selection view
204 [self _destroyAccountSelectionView];
206 [messageDisplayController messageViewIsClosing];
207 [messageDisplayController release];
208 [userListController release];
210 [controllerView_messages release];
212 //Release the views for which we are responsible (because we loaded them via -[NSBundle loadNibNamed:owner])
213 [nibrootView_messageView release];
214 [nibrootView_shelfVew release];
215 [nibrootView_userList release];
217 //Release the hidden user list view
218 if (retainingScrollViewUserList) {
219 [scrollView_userList release];
226 - (void)saveUserListMinimumSize
228 [[adium preferenceController] setPreference:[NSNumber numberWithInt:userListMinWidth]
229 forKey:KEY_ENTRY_USER_LIST_MIN_WIDTH
230 group:PREF_GROUP_DUAL_WINDOW_INTERFACE];
233 - (void)updateGradientColors
235 NSColor *darkerColor = [NSColor colorWithCalibratedWhite:0.90 alpha:1.0];
236 NSColor *lighterColor = [NSColor colorWithCalibratedWhite:0.92 alpha:1.0];
237 NSColor *leftColor = nil, *rightColor = nil;
239 switch ([messageWindowController tabPosition]) {
240 case AdiumTabPositionBottom:
241 case AdiumTabPositionTop:
242 case AdiumTabPositionLeft:
243 leftColor = lighterColor;
244 rightColor = darkerColor;
246 case AdiumTabPositionRight:
247 leftColor = darkerColor;
248 rightColor = lighterColor;
252 [view_accountSelection setLeftColor:leftColor rightColor:rightColor];
253 [splitView_textEntryHorizontal setLeftColor:leftColor rightColor:rightColor];
257 * @brief Invoked before the message view closes
259 * This method is invoked before our message view controller's message view leaves a window.
260 * We need to clean up our user list to invalidate cursor tracking before the view closes.
262 - (void)messageViewWillLeaveWindowController:(AIMessageWindowController *)inWindowController
264 if (inWindowController) {
265 [userListController contactListWillBeRemovedFromWindow];
268 [messageWindowController release]; messageWindowController = nil;
271 - (void)messageViewAddedToWindowController:(AIMessageWindowController *)inWindowController
273 if (inWindowController) {
274 [userListController contactListWasAddedBackToWindow];
277 if (inWindowController != messageWindowController) {
278 [messageWindowController release];
279 messageWindowController = [inWindowController retain];
281 [self updateGradientColors];
286 * @brief Retrieve the chat represented by this message view
294 * @brief Retrieve the source account associated with this chat
296 - (AIAccount *)account
298 return [chat account];
302 * @brief Retrieve the destination list object associated with this chat
304 - (AIListContact *)listObject
306 return [chat listObject];
310 * @brief Returns the selected list object in our participants list
312 - (AIListObject *)preferredListObject
314 if (userListView) { //[[shelfView subviews] containsObject:scrollView_userList] && ([userListView selectedRow] != -1)
315 return [userListView itemAtRow:[userListView selectedRow]];
322 * @brief Invoked when the status of our chat changes
324 * The only chat status change we're interested in is one to the disallow account switching flag. When this flag
325 * changes we update the visibility of our account status menus accordingly.
327 - (void)chatStatusChanged:(NSNotification *)notification
329 NSArray *modifiedKeys = [[notification userInfo] objectForKey:@"Keys"];
331 if (notification == nil || [modifiedKeys containsObject:@"DisallowAccountSwitching"]) {
332 [self setAccountSelectionMenuVisibleIfNeeded:YES];
337 //Message Display ------------------------------------------------------------------------------------------------------
338 #pragma mark Message Display
340 * @brief Configure the message display view
342 - (void)_configureMessageDisplay
344 //Create the message view
345 messageDisplayController = [[[adium interfaceController] messageDisplayControllerForChat:chat] retain];
346 //Get the messageView from the controller
347 controllerView_messages = [[messageDisplayController messageView] retain];
348 //scrollView_messages is originally a placeholder; replace it with controllerView_messages
349 [controllerView_messages setFrame:[scrollView_messages documentVisibleRect]];
350 [[customView_messages superview] replaceSubview:customView_messages with:controllerView_messages];
352 //This is what draws our transparent background
353 //Technically, it could be set in MessageView.nib, too
354 [scrollView_messages setBackgroundColor:[NSColor clearColor]];
356 [controllerView_messages setNextResponder:textView_outgoing];
360 * @brief Access to our view
364 return view_contents;
368 * @brief Support for printing. Forward the print command to our message display view
370 - (void)adiumPrint:(id)sender
372 if ([messageDisplayController respondsToSelector:@selector(adiumPrint:)]) {
373 [messageDisplayController adiumPrint:sender];
378 //Messaging ------------------------------------------------------------------------------------------------------------
379 #pragma mark Messaging
381 * @brief Send the entered message
383 - (IBAction)sendMessage:(id)sender
385 NSAttributedString *attributedString = [textView_outgoing textStorage];
387 //Only send if we have a non-zero-length string
388 if ([attributedString length] != 0) {
389 AIListObject *listObject = [chat listObject];
391 if ([chat isGroupChat] && ![[chat account] online]) {
392 //Refuse to do anything with a group chat for an offline account.
397 if (!suppressSendLaterPrompt &&
398 ![chat canSendMessages]) {
400 NSString *formattedUID = [listObject formattedUID];
402 NSAlert *alert = [[NSAlert alloc] init];
403 [alert setMessageText:[NSString stringWithFormat:AILocalizedString(@"%@ appears to be offline. How do you want to send this message?", nil),
405 [alert setInformativeText:[NSString stringWithFormat:
406 AILocalizedString(@"Send Later will send the message the next time both you and %@ are online. Send Now may work if %@ is invisible or is not on your contact list and so only appears to be offline.", "Send Later dialogue explanation text"),
407 formattedUID, formattedUID, formattedUID]];
408 [alert addButtonWithTitle:AILocalizedString(@"Send Now", nil)];
410 [alert addButtonWithTitle:AILocalizedString(@"Send Later", nil)];
411 [[[alert buttons] objectAtIndex:1] setKeyEquivalent:@"l"];
412 [[[alert buttons] objectAtIndex:1] setKeyEquivalentModifierMask:0];
414 [alert addButtonWithTitle:AILocalizedString(@"Don't Send", nil)];
415 [[[alert buttons] objectAtIndex:2] setKeyEquivalent:@"\E"];
416 [[[alert buttons] objectAtIndex:2] setKeyEquivalentModifierMask:0];
418 NSImage *icon = ([listObject userIcon] ? [listObject userIcon] : [AIServiceIcons serviceIconForObject:listObject
419 type:AIServiceIconLarge
420 direction:AIIconNormal]);
421 icon = [[icon copy] autorelease];
422 [icon setScalesWhenResized:NO];
423 [alert setIcon:icon];
424 [alert setAlertStyle:NSInformationalAlertStyle];
426 [alert beginSheetModalForWindow:[view_contents window]
428 didEndSelector:@selector(alertDidEnd:returnCode:contextInfo:)
434 AIContentMessage *message;
435 NSAttributedString *outgoingAttributedString;
436 AIAccount *account = [chat account];
438 [[adium notificationCenter] postNotificationName:Interface_WillSendEnteredMessage
442 outgoingAttributedString = [attributedString copy];
443 message = [AIContentMessage messageInChat:chat
445 destination:[chat listObject]
446 date:nil //created for us by AIContentMessage
447 message:outgoingAttributedString
449 [outgoingAttributedString release];
451 if ([[adium contentController] sendContentObject:message]) {
452 [[adium notificationCenter] postNotificationName:Interface_DidSendEnteredMessage
461 * @brief Send Later button was pressed
463 - (void)alertDidEnd:(NSAlert *)alert returnCode:(int)returnCode contextInfo:(void *)contextInfo
465 switch (returnCode) {
466 case NSAlertFirstButtonReturn: /* Send Now */
467 suppressSendLaterPrompt = YES;
468 [self sendMessage:nil];
471 case NSAlertSecondButtonReturn: /* Send Later */
472 [self sendMessageLater:nil];
474 case NSAlertThirdButtonReturn: /* Don't Send */
480 * @brief Invoked after our entered message sends
482 * This method hides the account selection view and clears the entered message after our message sends
484 - (IBAction)didSendMessage:(id)sender
486 [self setAccountSelectionMenuVisibleIfNeeded:NO];
487 [self clearTextEntryView];
489 //Redisplay the cursor
490 [NSCursor setHiddenUntilMouseMoves:NO];
494 * @brief Offline messaging
496 - (IBAction)sendMessageLater:(id)sender
498 AIListContact *listContact;
500 //If the chat can _now_ send a message, send it immediately instead of waiting for "later".
501 if ([chat canSendMessages]) {
502 [self sendMessage:sender];
506 //Put the alert on the metaContact containing this listContact if applicable
507 listContact = [[chat listObject] parentContact];
510 NSMutableDictionary *detailsDict, *alertDict;
512 detailsDict = [NSMutableDictionary dictionary];
513 [detailsDict setObject:[[chat account] internalObjectID] forKey:@"Account ID"];
514 [detailsDict setObject:[NSNumber numberWithBool:YES] forKey:@"Allow Other"];
515 [detailsDict setObject:[listContact internalObjectID] forKey:@"Destination ID"];
517 alertDict = [NSMutableDictionary dictionary];
518 [alertDict setObject:detailsDict forKey:@"ActionDetails"];
519 [alertDict setObject:CONTACT_SEEN_ONLINE_YES forKey:@"EventID"];
520 [alertDict setObject:@"SendMessage" forKey:@"ActionID"];
521 [alertDict setObject:[NSNumber numberWithBool:YES] forKey:@"OneTime"];
523 [alertDict setObject:listContact forKey:@"TEMP-ListContact"];
525 [[adium contentController] filterAttributedString:[[[textView_outgoing textStorage] copy] autorelease]
526 usingFilterType:AIFilterContent
527 direction:AIFilterOutgoing
528 filterContext:listContact
530 selector:@selector(gotFilteredMessageToSendLater:receivingContext:)
533 [self didSendMessage:nil];
538 * @brief Offline messaging
540 //XXX - Offline messaging code SHOULD NOT BE IN HERE! -ai
541 - (void)gotFilteredMessageToSendLater:(NSAttributedString *)filteredMessage receivingContext:(NSMutableDictionary *)alertDict
543 NSMutableDictionary *detailsDict;
544 AIListContact *listContact;
546 detailsDict = [alertDict objectForKey:@"ActionDetails"];
547 [detailsDict setObject:[filteredMessage dataRepresentation] forKey:@"Message"];
549 listContact = [[alertDict objectForKey:@"TEMP-ListContact"] retain];
550 [alertDict removeObjectForKey:@"TEMP-ListContact"];
552 [[adium contactAlertsController] addAlert:alertDict
553 toListObject:listContact
554 setAsNewDefaults:NO];
555 [listContact release];
558 //Account Selection ----------------------------------------------------------------------------------------------------
559 #pragma mark Account Selection
563 - (void)accountSelectionViewFrameDidChange:(NSNotification *)notification
565 [self updateFramesForAccountSelectionView];
569 * @brief Redisplay the source/destination account selector
571 - (void)redisplaySourceAndDestinationSelector:(NSNotification *)notification
573 [self setAccountSelectionMenuVisibleIfNeeded:YES];
577 * @brief Toggle visibility of the account selection menus
579 * Invoking this method with NO will hide the account selection menus. Invoking it with YES will show the account
580 * selection menus if they are needed.
582 - (void)setAccountSelectionMenuVisibleIfNeeded:(BOOL)makeVisible
584 //Hide or show the account selection view as requested
586 [self _createAccountSelectionView];
588 [self _destroyAccountSelectionView];
593 * @brief Show the account selection view
595 - (void)_createAccountSelectionView
597 if (!view_accountSelection) {
598 NSRect contentFrame = [splitView_textEntryHorizontal frame];
600 //Create the account selection view and insert it into our window
601 view_accountSelection = [[AIAccountSelectionView alloc] initWithFrame:contentFrame];
603 [view_accountSelection setAutoresizingMask:(NSViewWidthSizable | NSViewMinYMargin)];
605 [self updateGradientColors];
607 //Insert the account selection view at the top of our view
608 [[shelfView contentView] addSubview:view_accountSelection];
609 [view_accountSelection setChat:chat];
611 [[NSNotificationCenter defaultCenter] addObserver:self
612 selector:@selector(accountSelectionViewFrameDidChange:)
613 name:AIViewFrameDidChangeNotification
614 object:view_accountSelection];
616 [self updateFramesForAccountSelectionView];
618 //Redisplay everything
619 [[shelfView contentView] setNeedsDisplay:YES];
624 * @brief Hide the account selection view
626 - (void)_destroyAccountSelectionView
628 if (view_accountSelection) {
629 //Remove the observer
630 [[NSNotificationCenter defaultCenter] removeObserver:self
631 name:AIViewFrameDidChangeNotification
632 object:view_accountSelection];
634 //Remove the account selection view from our window, clean it up
635 [view_accountSelection removeFromSuperview];
636 [view_accountSelection release]; view_accountSelection = nil;
638 //Redisplay everything
639 [self updateFramesForAccountSelectionView];
644 * @brief Position the account selection view, if it is present, and the messages/text entry splitview appropriately
646 - (void)updateFramesForAccountSelectionView
648 int contentsHeight = [[shelfView contentView] frame].size.height;
649 int accountSelectionHeight = (view_accountSelection ? [view_accountSelection frame].size.height : 0);
650 int intersectionPoint = ([[shelfView contentView] isFlipped] ? accountSelectionHeight : (contentsHeight - accountSelectionHeight));
652 if (view_accountSelection) {
653 [view_accountSelection setFrameOrigin:NSMakePoint(NSMinX([view_accountSelection frame]), intersectionPoint)];
654 [view_accountSelection setNeedsDisplay:YES];
657 [splitView_textEntryHorizontal setFrameSize:NSMakeSize(NSWidth([splitView_textEntryHorizontal frame]), intersectionPoint)];
658 [splitView_textEntryHorizontal setNeedsDisplay:YES];
662 //Text Entry -----------------------------------------------------------------------------------------------------------
663 #pragma mark Text Entry
665 * @brief Preferences changed, update sending keys
667 - (void)preferencesChangedForGroup:(NSString *)group key:(NSString *)key object:(AIListObject *)object
668 preferenceDict:(NSDictionary *)prefDict firstTime:(BOOL)firstTime
670 [textView_outgoing setSendOnReturn:[[prefDict objectForKey:SEND_ON_RETURN] boolValue]];
671 [textView_outgoing setSendOnEnter:[[prefDict objectForKey:SEND_ON_ENTER] boolValue]];
675 * @brief Configure the text entry view
677 - (void)_configureTextEntryView
679 //Configure the text entry view
680 [textView_outgoing setTarget:self action:@selector(sendMessage:)];
682 //This is necessary for tab completion.
683 [textView_outgoing setDelegate:self];
685 [textView_outgoing setTextContainerInset:NSMakeSize(0,2)];
686 if ([textView_outgoing respondsToSelector:@selector(setUsesFindPanel:)]) {
687 [textView_outgoing setUsesFindPanel:YES];
689 [textView_outgoing setClearOnEscape:YES];
690 [textView_outgoing setTypingAttributes:[[adium contentController] defaultFormattingAttributes]];
692 //User's choice of mininum height for their text entry view
693 entryMinHeight = [[[adium preferenceController] preferenceForKey:KEY_ENTRY_TEXTVIEW_MIN_HEIGHT
694 group:PREF_GROUP_DUAL_WINDOW_INTERFACE] intValue];
695 if (entryMinHeight <= 0) entryMinHeight = [self _textEntryViewProperHeightIgnoringUserMininum:YES];
697 //Associate the view with our message view so it knows which view to scroll in response to page up/down
698 //and other special key-presses.
699 [textView_outgoing setAssociatedView:[messageDisplayController messageScrollView]];
701 //Associate the text entry view with our chat and inform Adium that it exists.
702 //This is necessary for text entry filters to work correctly.
703 [textView_outgoing setChat:chat];
705 //Observe text entry view size changes so we can dynamically resize as the user enters text
706 [[NSNotificationCenter defaultCenter] addObserver:self
707 selector:@selector(outgoingTextViewDesiredSizeDidChange:)
708 name:AIViewDesiredSizeDidChangeNotification
709 object:textView_outgoing];
711 [self _updateTextEntryViewHeight];
715 * @brief Sets our text entry view as the first responder
717 - (void)makeTextEntryViewFirstResponder
719 [[textView_outgoing window] makeFirstResponder:textView_outgoing];
723 * @brief Clear the message entry text view
725 - (void)clearTextEntryView
727 NSWritingDirection writingDirection;
729 writingDirection = [textView_outgoing baseWritingDirection];
731 [textView_outgoing setString:@""];
732 [textView_outgoing setTypingAttributes:[[adium contentController] defaultFormattingAttributes]];
734 [textView_outgoing setBaseWritingDirection:writingDirection]; //Preserve the writing diraction
736 [[NSNotificationCenter defaultCenter] postNotificationName:NSTextDidChangeNotification
737 object:textView_outgoing];
741 * @brief Add text to the message entry text view
743 * Adds the passed string to the entry text view at the insertion point. If there is selected text in the view, it
746 - (void)addToTextEntryView:(NSAttributedString *)inString
748 [textView_outgoing insertText:inString];
749 [[NSNotificationCenter defaultCenter] postNotificationName:NSTextDidChangeNotification object:textView_outgoing];
753 * @brief Add data to the message entry text view
755 * Adds the passed pasteboard data to the entry text view at the insertion point. If there is selected text in the
756 * view, it will be replaced.
758 - (void)addDraggedDataToTextEntryView:(id <NSDraggingInfo>)draggingInfo
760 [textView_outgoing performDragOperation:draggingInfo];
761 [[NSNotificationCenter defaultCenter] postNotificationName:NSTextDidChangeNotification object:textView_outgoing];
765 * @brief Update the text entry view's height when its desired size changes
767 - (void)outgoingTextViewDesiredSizeDidChange:(NSNotification *)notification
769 [self _updateTextEntryViewHeight];
772 - (void)tabViewDidChangeVisibility
774 [self _updateTextEntryViewHeight];
778 * @brief Update the height of our text entry view
780 * This method sets the height of the text entry view to the most ideal value, and adjusts the other views in our
781 * window to fill the remaining space.
783 - (void)_updateTextEntryViewHeight
785 int height = [self _textEntryViewProperHeightIgnoringUserMininum:NO];
787 //Display the vertical scroller if our view is not tall enough to display all the entered text
788 [scrollView_outgoing setHasVerticalScroller:(height < [textView_outgoing desiredSize].height)];
790 if ([NSApp isOnLeopardOrBetter]) {
791 //Attempt to maximize the message view's size. We'll automatically restrict it to the correct minimum via the NSSplitView's delegate methods.
792 [splitView_textEntryHorizontal adjustSubviews];
793 [splitView_textEntryHorizontal setPosition:(NSHeight([splitView_textEntryHorizontal frame]) - height)
797 NSRect tempFrame, newFrame;
800 //Size the outgoing text view to the desired height
801 tempFrame = [scrollView_outgoing frame];
802 newFrame = NSMakeRect(tempFrame.origin.x,
803 [splitView_textEntryHorizontal frame].size.height - height,
804 tempFrame.size.width,
806 if (!NSEqualRects(tempFrame, newFrame)) {
807 [scrollView_outgoing setFrame:newFrame];
808 [scrollView_outgoing setNeedsDisplay:YES];
813 [splitView_textEntryHorizontal adjustSubviews];
819 * @brief Returns the height our text entry view should be
821 * This method takes into account user preference, the amount of entered text, and the current window size to return
822 * a height which is most ideal for the text entry view.
824 * @param ignoreUserMininum If YES, the user's preference for mininum height will be ignored
826 - (int)_textEntryViewProperHeightIgnoringUserMininum:(BOOL)ignoreUserMininum
828 int dividerThickness = [splitView_textEntryHorizontal dividerThickness];
829 int allowedHeight = ([splitView_textEntryHorizontal frame].size.height / 2.0) - dividerThickness;
832 //Our primary goal is to display all the entered text
833 height = [textView_outgoing desiredSize].height;
835 //But we must never fall below the user's prefered mininum or above the allowed height
836 if (!ignoreUserMininum && height < entryMinHeight) {
837 height = entryMinHeight;
839 if (height > allowedHeight) height = allowedHeight;
844 #pragma mark Autocompletion
846 * @brief Should the tab key cause an autocompletion if possible?
848 * We only tab to autocomplete for a group chat
850 - (BOOL)textViewShouldTabComplete:(NSTextView *)inTextView
852 return [[self chat] isGroupChat];
855 - (NSArray *)textView:(NSTextView *)textView completions:(NSArray *)words forPartialWordRange:(NSRange)charRange indexOfSelectedItem:(int *)index
857 NSMutableArray *completions;
859 if ([[self chat] isGroupChat]) {
860 NSString *partialWord = [[[textView textStorage] attributedSubstringFromRange:charRange] string];
861 NSEnumerator *enumerator;
862 AIListContact *listContact;
865 if (charRange.location == 0) {
866 //At the start of a line, append ": "
872 completions = [NSMutableArray array];
873 enumerator = [[[self chat] containedObjects] objectEnumerator];
874 while ((listContact = [enumerator nextObject])) {
875 if ([[listContact displayName] rangeOfString:partialWord
876 options:(NSLiteralSearch | NSAnchoredSearch)].location != NSNotFound) {
878 [completions addObject:(suffix ? [[listContact displayName] stringByAppendingString:suffix] : [listContact displayName])];
880 } else if ([[listContact formattedUID] rangeOfString:partialWord
881 options:(NSLiteralSearch | NSAnchoredSearch)].location != NSNotFound) {
882 [completions addObject:(suffix ? [[listContact formattedUID] stringByAppendingString:suffix] : [listContact formattedUID])];
884 } else if ([[listContact UID] rangeOfString:partialWord
885 options:(NSLiteralSearch | NSAnchoredSearch)].location != NSNotFound) {
886 [completions addObject:(suffix ? [[listContact UID] stringByAppendingString:suffix] : [listContact UID])];
890 if ([completions count]) {
898 return ([completions count] ? completions : words);
901 //User List ------------------------------------------------------------------------------------------------------------
902 #pragma mark User List
904 * @brief Set visibility of the user list
906 - (void)setUserListVisible:(BOOL)inVisible
909 [self _showUserListView];
911 [self _hideUserListView];
916 * @brief Returns YES if the user list is currently visible
918 - (BOOL)userListVisible
920 return [shelfView isShelfVisible];
924 * @brief Show the user list
926 - (void)_showUserListView
928 [self setupShelfView];
930 //Configure the user list
931 [self _configureUserList];
933 //Add the user list back to our window if it's missing
934 if (![self userListVisible]) {
935 [self _updateUserListViewWidth];
937 if (retainingScrollViewUserList) {
938 [scrollView_userList release];
939 retainingScrollViewUserList = NO;
945 * @brief Hide the user list.
947 * We gain responsibility for releasing scrollView_userList after we hide it
949 - (void)_hideUserListView
951 if ([self userListVisible]) {
952 [scrollView_userList retain];
953 [scrollView_userList removeFromSuperview];
954 retainingScrollViewUserList = YES;
956 [self saveUserListMinimumSize];
957 [userListController release];
958 userListController = nil;
960 //need to collapse the splitview
961 [shelfView setShelfIsVisible:NO];
966 * @brief Configure the user list
968 * Configures the user list view and prepares it for display. If the user list is not being shown, this configuration
969 * should be avoided for performance.
971 - (void)_configureUserList
973 if (!userListController) {
974 NSDictionary *themeDict = [NSDictionary dictionaryNamed:USERLIST_THEME forClass:[self class]];
975 NSDictionary *layoutDict = [NSDictionary dictionaryNamed:USERLIST_LAYOUT forClass:[self class]];
977 //Create and configure a controller to manage the user list
978 userListController = [[ESChatUserListController alloc] initWithContactListView:userListView
979 inScrollView:scrollView_userList
981 [userListController updateLayoutFromPrefDict:layoutDict andThemeFromPrefDict:themeDict];
982 [userListController setContactListRoot:chat];
983 [userListController setHideRoot:YES];
985 //User's choice of mininum width for their user list view
986 userListMinWidth = [[[adium preferenceController] preferenceForKey:KEY_ENTRY_USER_LIST_MIN_WIDTH
987 group:PREF_GROUP_DUAL_WINDOW_INTERFACE] intValue];
988 if (userListMinWidth < USER_LIST_MIN_WIDTH) userListMinWidth = USER_LIST_DEFAULT_WIDTH;
989 [shelfView setShelfWidth:[userListView bounds].size.width];
994 * @brief Update the user list in response to changes
996 * This method is invoked when the chat's participating contacts change. In resopnse, it sets correct visibility of
997 * the user list, and updates the displayed users.
999 - (void)chatParticipatingListObjectsChanged:(NSNotification *)notification
1001 //Update the user list
1002 AILogWithSignature(@"%i, so %@ %@",[self userListVisible], ([self userListVisible] ? @"reloading" : @"not reloading"),
1003 userListController);
1004 if ([self userListVisible]) {
1005 [userListController reloadData];
1010 * @brief The selection in the user list changed
1012 * When the user list selection changes, we update the chat's "preferred list object", which is used
1013 * elsewhere to identify the currently 'selected' contact for Get Info, Messaging, etc.
1015 - (void)outlineViewSelectionDidChange:(NSNotification *)notification
1017 if ([notification object] == userListView) {
1018 int selectedIndex = [userListView selectedRow];
1019 [chat setPreferredListObject:((selectedIndex != -1) ?
1020 [[chat containedObjects] objectAtIndex:selectedIndex] :
1026 * @brief Perform default action on the selected user list object
1028 * Here we could open a private message or display info for the user, however we perform no action
1031 - (void)performDefaultActionOnSelectedObject:(AIListObject *)listObject sender:(NSOutlineView *)sender
1037 * @brief Update the width of our user list view
1039 * This method sets the width of the user list view to the most ideal value, and adjusts the other views in our
1040 * window to fill the remaining space.
1042 - (void)_updateUserListViewWidth
1044 int width = [self _userListViewProperWidthIgnoringUserMininum:NO];
1045 int widthWithDivider = 1 + width; //resize bar effective width
1048 //Size the user list view to the desired width
1049 tempFrame = [scrollView_userList frame];
1050 [scrollView_userList setFrame:NSMakeRect([shelfView frame].size.width - width,
1053 tempFrame.size.height)];
1055 //Size the message view to fill the remaining space
1056 tempFrame = [scrollView_messages frame];
1057 [scrollView_messages setFrame:NSMakeRect(tempFrame.origin.x,
1059 [shelfView frame].size.width - widthWithDivider,
1060 tempFrame.size.height)];
1062 //Redisplay both views and the divider
1063 [shelfView setNeedsDisplay:YES];
1067 * @brief Returns the width our user list view should be
1069 * This method takes into account user preference and the current window size to return a width which is most
1070 * ideal for the user list view.
1072 * @param ignoreUserMininum If YES, the user's preference for mininum width will be ignored
1074 - (int)_userListViewProperWidthIgnoringUserMininum:(BOOL)ignoreUserMininum
1076 int dividerThickness = 1; //[shelfView dividerThickness];
1077 int allowedWidth = ([shelfView frame].size.width / 2.0) - dividerThickness;
1078 int width = USER_LIST_MIN_WIDTH;
1080 //We must never fall below the user's prefered mininum or above the allowed width
1081 if (!ignoreUserMininum && width < userListMinWidth) width = userListMinWidth;
1082 if (width > allowedWidth) width = allowedWidth;
1088 //Split Views --------------------------------------------------------------------------------------------------
1089 #pragma mark Split Views
1091 * @brief Returns the maximum constraint of the split pane
1093 * For the horizontal split, we prevent the message view from growing so large that the text entry view
1094 * is forced below its desired height.
1096 - (float)splitView:(NSSplitView *)sender constrainMaxCoordinate:(float)proposedMax ofSubviewAt:(int)offset
1098 if (sender == splitView_textEntryHorizontal) {
1099 return ([sender frame].size.height - ([self _textEntryViewProperHeightIgnoringUserMininum:YES] +
1100 [sender dividerThickness]));
1103 NSLog(@"Unknown split view %@",sender);
1109 * @brief Returns the mininum constraint of the split pane
1111 * For both splitpanes, we prevent the message view from dropping below 50% of the window's width and height
1113 - (float)splitView:(NSSplitView *)sender constrainMinCoordinate:(float)proposedMin ofSubviewAt:(int)offset
1115 if (sender == splitView_textEntryHorizontal) {
1116 return (int)([sender frame].size.height * MESSAGE_VIEW_MIN_HEIGHT_RATIO);
1119 NSLog(@"Unknown split view %@",sender);
1125 * @brief A split view had its divider position changed
1127 * Remember the user's choice of text entry view height.
1129 - (float)splitView:(NSSplitView *)sender constrainSplitPosition:(float)proposedPosition ofSubviewAt:(int)index
1131 if (sender == splitView_textEntryHorizontal) {
1132 entryMinHeight = (int)([sender frame].size.height - proposedPosition);
1134 NSLog(@"Unknown split view %@",sender);
1138 return proposedPosition;
1142 * @brief Returns YES if the passed subview can be collapsed
1144 - (BOOL)splitView:(NSSplitView *)sender canCollapseSubview:(NSView *)subview
1146 if (sender == splitView_textEntryHorizontal) {
1150 NSLog(@"Unknown split view %@",sender);
1155 #pragma mark Shelfview
1156 /* @name setupShelfView
1157 * @brief sets up shelfsplitview containing userlist & contentviews
1159 -(void)setupShelfView
1161 [shelfView setShelfWidth:200];
1163 AILogWithSignature(@"ShelfView %@ (content view is %@) --> superview %@, in window %@; frame %@; content view %@ shelf view %@ in window %@",
1164 shelfView, [shelfView contentView], [shelfView superview], [shelfView window], NSStringFromRect([[shelfView superview] frame]),
1165 splitView_textEntryHorizontal,
1166 scrollView_userList, [scrollView_userList window]);
1168 [shelfView bind:@"contextButtonMenu" toObject:[self chat] withKeyPath:@"actionMenu"
1169 options:[NSDictionary dictionaryWithObjectsAndKeys:
1170 [NSNumber numberWithBool:YES], NSAllowsNullArgumentBindingOption,
1171 [NSNumber numberWithBool:YES], NSValidatesImmediatelyBindingOption,
1173 [shelfView setContextButtonImage:[NSImage imageNamed:@"sidebarActionWidget.png"]];
1175 [shelfView setShelfIsVisible:YES];
1178 /* @name toggleUserlist
1179 * @brief toggles the state of the userlist shelf
1181 -(void)toggleUserlist:(id)sender
1183 [shelfView setShelfIsVisible:![shelfView isShelfVisible]];